package cn.wolfcode.service.impl;

import cn.wolfcode.common.exception.BusinessException;
import cn.wolfcode.common.utils.AssertUtil;
import cn.wolfcode.common.utils.IdGenerateUtil;
import cn.wolfcode.common.web.CodeMsg;
import cn.wolfcode.domain.AccountLog;
import cn.wolfcode.domain.AccountTransaction;
import cn.wolfcode.domain.IntergralPayResult;
import cn.wolfcode.domain.OperateIntergralVo;
import cn.wolfcode.mapper.AccountLogMapper;
import cn.wolfcode.mapper.AccountTransactionMapper;
import cn.wolfcode.mapper.UsableIntegralMapper;
import cn.wolfcode.service.IUsableIntegralService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;

@Slf4j
@Service
public class UsableIntegralServiceImpl implements IUsableIntegralService {
    private IdGenerateUtil idGenerateUtil;
    private final UsableIntegralMapper usableIntegralMapper;
    private final AccountTransactionMapper accountTransactionMapper;
    private final AccountLogMapper accountLogMapper;


    public UsableIntegralServiceImpl(IdGenerateUtil idGenerateUtil, UsableIntegralMapper usableIntegralMapper, AccountTransactionMapper accountTransactionMapper, AccountLogMapper accountLogMapper) {
        this.idGenerateUtil = idGenerateUtil;
        this.usableIntegralMapper = usableIntegralMapper;
        this.accountTransactionMapper = accountTransactionMapper;
        this.accountLogMapper = accountLogMapper;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public IntergralPayResult tryIntergralPay(BusinessActionContext ctx, OperateIntergralVo intergralVo) {
        log.info("[TCC-TRY] 一阶段，收到资源预留信息，准备进行资源预留：{}", JSON.toJSONString(intergralVo));
        //1.预留资源，添加冻结积分
        int freeRow = usableIntegralMapper.freezeIntergral(intergralVo.getUserId(), intergralVo.getValue());
        AssertUtil.isTrue(freeRow > 0, "用户积分不足");
        //2.生成积分支付流水号，通过事务上下文对象传递给commit
        String tradeNo = idGenerateUtil.nextId() + "";
        BusinessActionContextUtil.addContext("tradeNo", tradeNo);
        //3.封装支付结果
        IntergralPayResult intergralPayResult = new IntergralPayResult();
        intergralPayResult.setTradeNo(tradeNo);
        intergralPayResult.setOutTradeNo(intergralVo.getOutTradeNo());
        intergralPayResult.setAmount(intergralVo.getValue());
        return intergralPayResult;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void confirmIntergralPay(BusinessActionContext ctx) {
        OperateIntergralVo intergralVo = ctx.getActionContext("intergralVo", OperateIntergralVo.class);
        String tradeNo = ctx.getActionContext("tradeNo", String.class);
        log.info("[TCC-CONFIRM] 二阶段，收到提交信息，准备进行提交：intergralVo={},tradeNo={}", JSON.toJSONString(intergralVo),tradeNo);
        //1.扣除冻结积分，以及原积分
        int commitChangeRow = usableIntegralMapper.commitChange(intergralVo.getUserId(), intergralVo.getValue());
        if (commitChangeRow <= 0) {
            log.warn("[TCC-COMFIRM] 二阶段，资源提交失败");
        }
        //2.积分扣除成功后，记录积分变动日志
        AccountLog accountLog = this.buildAccountLog(intergralVo, AccountLog.TYPE_DECR);
        accountLog.setTradeNo(tradeNo);
        int accountLogRow = accountLogMapper.insert(accountLog);
        if (accountLogRow <= 0) {
            log.warn("[TCC-CONFIRM] 二阶段,积分变动日志记录失败");
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void cancelIntergralPay(BusinessActionContext ctx) {
        OperateIntergralVo intergralVo = ctx.getActionContext("intergralVo", OperateIntergralVo.class);
        log.info("[TCC-CANCEL] 二阶段，收到资源回滚信息，准备进行回滚操作");
        //1.回滚一阶段try的预留资源 即 预留积分
        int cancelRow = usableIntegralMapper.unFreezeIntergral(intergralVo.getUserId(), intergralVo.getValue());
        if (cancelRow <= 0) {
            log.warn("[TCC-CANCEL] 二阶段,资源回滚失败");
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public IntergralPayResult tryPayment(OperateIntergralVo intergralVo, BusinessActionContext ctx) {
        log.info("[TCC-TRY] 收到请求参数: {}，准备执行 tryPayment,ctx={}", JSON.toJSONString(intergralVo), JSON.toJSONString(ctx.getActionContext()));
        //防悬挂：也就是由于各种原因导致cancel比try阶段先执行，
        //1.实现防悬挂：向MySQL插入事务控制记录，并状态设置为初始化，如果日志插入失败，则为已回滚，则不往下执行try业务逻辑
        try {
            this.txInsert(intergralVo, ctx, String.valueOf(AccountLog.TYPE_DECR), AccountTransaction.STATE_TRY);
        } catch (Exception e) {
            throw new BusinessException(new CodeMsg(5005003, "事务控制记录插入失败"));
        }

        //2.冻结积分 ，前提：总积分-冻结积分>=冻结积分
        int row = usableIntegralMapper.freezeIntergral(intergralVo.getUserId(), intergralVo.getValue());
        AssertUtil.isTrue(row > 0, "账户积分不足");

        //生成订单交易流水号
        String tradeNo = idGenerateUtil.nextId() + "";
        //将 tradeNo 通过上下文对象，传递给 commit 方法
        BusinessActionContextUtil.addContext("tradeNo", tradeNo);

        //封装支付结果
        IntergralPayResult intergralPayResult = new IntergralPayResult();
        intergralPayResult.setTradeNo(tradeNo);
        intergralPayResult.setOutTradeNo(intergralVo.getOutTradeNo());
        intergralPayResult.setAmount(intergralVo.getValue());
        return intergralPayResult;
    }

    //插入事务记录
    private void txInsert(OperateIntergralVo intergralVo, BusinessActionContext ctx, String type, int state) {
        AccountTransaction accountTransaction = new AccountTransaction();
        accountTransaction.setTxId(ctx.getXid());
        accountTransaction.setActionId(String.valueOf(ctx.getBranchId()));
        Date now = new Date();
        accountTransaction.setGmtCreated(now);
        accountTransaction.setGmtModified(now);
        accountTransaction.setAmount(intergralVo.getValue());
        accountTransaction.setUserId(intergralVo.getUserId());
        accountTransaction.setType(type);
        accountTransaction.setState(state);
        accountTransactionMapper.insert(accountTransaction);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void commitPayment(BusinessActionContext ctx) {
        //ctx.getActionContext(paramName) 获取try阶段贴 @BusinessActionContextParameter 注解的值
        Object obj = ctx.getActionContext("intergralVo");
        //从上下文中获取 try 阶段传递的 tradeNo
        String tradeNo = (String) ctx.getActionContext("tradeNo");
        log.info("[TCC-CONFIRM] 收到 TCC CONFIRM 消息 xid={},branchId={}, 准备执行业务并提交本地事务:{},ctx={}", ctx.getXid(), ctx.getBranchId(), obj, ctx.getActionContext());
        JSONObject intergralVoJson = (JSONObject) obj;
        OperateIntergralVo intergralVo = intergralVoJson.toJavaObject(OperateIntergralVo.class);
        // 1. 根据全局事务id以及分支事务id查询积分事务表的唯一事务记录
        AccountTransaction accountTransaction = accountTransactionMapper.get(ctx.getXid(), String.valueOf(ctx.getBranchId()));
        // 2. 如果没有该事务记录，则异常，说明一阶段没有执行过,注意：不能抛异常，因为成功执行try，进入到commit，就认为commit一定执行成功，如果抛异常，则会认为commit执行失败，则会不断重新
        if (accountTransaction == null) {
            log.warn("[TCC-CONFIRM] 流程异常，无法查询到 TRY 阶段事务记录，终止 CONFIRM 流程 xid={},branchId={}", ctx.getXid(), ctx.getBranchId());
            return;
        }
        // 3. 判断事务记录是否为已提交，如果是，说明已经提交了，保证幂等即可，直接结束，后面代码不在执行
        if (accountTransaction.getState() == AccountTransaction.STATE_COMMIT) {
            log.warn("[TCC-CONFIRM] 流程异常，CONFIRM 方法被重复调用 xid={},branchId={}", ctx.getXid(), ctx.getBranchId());
            return;
        } else if (accountTransaction.getState() == AccountTransaction.STATE_CANCEL) {
            // 4. 判断事务记录是否为已回滚，如果是，说明流程异常，直接结束，后面代码不在执行
            log.warn("[TCC-CONFIRM] 流程异常,已经执行过 CANCEL 方法，无法再执行 CONFIRM 方法 xid={},branchId={}", ctx.getXid(), ctx.getBranchId());
            return;
        }
        //此时，表示为已经初始化阶段，则进行commit操作
        // 5. 扣除真实积分以及冻结积分
        int changeRow = usableIntegralMapper.commitChange(intergralVo.getUserId(), intergralVo.getValue());
        if (changeRow <= 0) {
            log.warn("[TCC-CONFIRM] 原积分与冻结积分扣除失败 xid={},branchId={}", ctx.getXid(), ctx.getBranchId());
            return;
        }
        //6.记录积分扣除记录表
        AccountLog accountLog = this.buildAccountLog(intergralVo, AccountLog.TYPE_DECR);

        accountLog.setTradeNo(tradeNo);
        int logRow = accountLogMapper.insert(accountLog);
        if (logRow <= 0) {
            log.warn("[TCC-CONFIRM] 记录积分变动失败:{}", JSON.toJSONString(accountLog));
            return;
        }
        //7.更新状态事务状态为已提交
        int row = accountTransactionMapper.updateAccountTransactionState(
                ctx.getXid(),
                String.valueOf(ctx.getBranchId()),
                AccountTransaction.STATE_COMMIT,
                AccountTransaction.STATE_TRY);
        //注意：不能抛异常，否则会导致重试调用该commit方法，但是又要让前面的回滚，如果重试，最后通过告警处理，或人工处理
        log.info("[TCC-CONFIRM] 积分支付提交操作执行完成:{}", row);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void rollbackPayment(BusinessActionContext ctx) {
        Object obj = ctx.getActionContext("intergralVo");
        log.info("[TCC-ROLLBACK] 收到 TCC ROLLBACK 消息 xid={},branchId={}, 准备执行回滚事务:{}", ctx.getXid(), ctx.getBranchId(), obj);
        JSONObject intergralVoJson = (JSONObject) obj;
        OperateIntergralVo intergralVo = intergralVoJson.toJavaObject(OperateIntergralVo.class);
        // 1. 根据全局事务id以及分支事务id查询积分事务表的唯一事务记录
        AccountTransaction accountTransaction = accountTransactionMapper.get(ctx.getXid(), String.valueOf(ctx.getBranchId()));
        // 2. 如果没有该事务记录，说明一阶段没有执行过（空回滚），插入MySQL事务记录，并终止该方法
        if (accountTransaction == null) {
            try {
                log.warn("[TCC-ROLLBACK] 流程异常，出现空回滚问题，TRY阶段未执行，直接执行 CANCEL 阶段 xid={},branchId={}, 准备执行回滚事务:{}", ctx.getXid(), ctx.getBranchId(), obj);
                this.txInsert(intergralVo, ctx, AccountLog.TYPE_INCR + "", AccountTransaction.STATE_CANCEL);
            } catch (Exception e) {
                log.warn("[TCC-ROLLBACK] 流程异常，出现空回滚并发问题，TRY阶段未执行，直接执行 CANCEL 阶段 xid={},branchId={}, 准备执行回滚事务:{}", ctx.getXid(), ctx.getBranchId(), obj);
                log.error("[TCC-ROLLBACK] 打印流程异常信息", e);
            }
            //出现空回滚之后，无论是否出现异常，都应该直接终止该方法
            return;
        }
        //3.如果有该事务记录，判断事务是否已回滚，如果已回滚，说明重复执行回滚操作，直接终止该方法
        if (accountTransaction.getState() == AccountTransaction.STATE_CANCEL) {
            log.warn("[TCC-ROLLBACK] 流程异常，出现空回滚重复执行问题，TRY阶段未执行，直接终止该方法 xid={},branchId={}", ctx.getXid(), ctx.getBranchId());
            return;
        } else if (accountTransaction.getState() == AccountTransaction.STATE_COMMIT) {
            //4.否则判断是否为已提交，如果是，说明流程异常，终止方法
            log.warn("[TCC-ROLLBACK] 流程异常，已经执行过CONFIRM方法，不在执行CANCEL方法 xid={},branchId={}", ctx.getXid(), ctx.getBranchId());
            return;
        }
        //5.如果是初始化方法，正常流程，将之前 TRY 执行的冻结积分回滚回去
        try {
            usableIntegralMapper.unFreezeIntergral(intergralVo.getUserId(), intergralVo.getValue());
        } catch (Exception e) {
            log.warn("[TCC-ROLLBACK] 流程异常，冻结积分回滚失败");
        }
        //6.把状态改为 CANCEL 状态
        int row = accountTransactionMapper.updateAccountTransactionState(
                ctx.getXid(),
                ctx.getBranchId() + "",
                AccountTransaction.STATE_CANCEL,
                AccountTransaction.STATE_TRY);
        log.info("[TCC-CANCEL] 更新事务记录状态为已回滚:{}", row);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public IntergralPayResult pay(OperateIntergralVo intergralVo) {
        //1.在进行支付前，先判断该用户积分是否足够
        //2.进行积分扣除
        //注意：判断积分是否足够以及积分扣除，必须保证原子性操作，否则会有线程安全问题
        //解决方案：1.分布式锁 2.乐观锁
        //本方案是：乐观锁，在更新时，加乐观锁条件
        //真实积分 = 积分-冻结金额 = amount - freezedAmount
        int decrRow = usableIntegralMapper.decrAmountByUserId(intergralVo.getUserId(), intergralVo.getValue());
        AssertUtil.isTrue(decrRow > 0, "账户积分不足");
        //3.记录积分扣除记录
        //注意：由于Feign有重试机制，那扣积分以及积分记录存在幂等性问题
        //如：当积分足够，每次都能扣成功，并且都能成功记录日志
        //解决幂等性问题：MySQL唯一索引,订单号唯一，但是扣与加积分是两个不同操作，所以：订单号+type为唯一索引
        AccountLog accountLog = this.buildAccountLog(intergralVo, AccountLog.TYPE_DECR);
        int logSaveRow = accountLogMapper.insert(accountLog);
        AssertUtil.isTrue(logSaveRow > 0, "积分日志记录失败");
        //4.返回支付结果
        IntergralPayResult intergralPayResult = new IntergralPayResult();
        intergralPayResult.setTradeNo(accountLog.getTradeNo());
        intergralPayResult.setOutTradeNo(accountLog.getOutTradeNo());
        intergralPayResult.setAmount(accountLog.getAmount());
        return intergralPayResult;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public Boolean refund(OperateIntergralVo intergralVo) {
        //1.判断是否已经积分支付(订单编号,本人支付,类型：支付)
        AccountLog decrLog = accountLogMapper.findByoutTradeNoAndType(intergralVo.getOutTradeNo(), AccountLog.TYPE_DECR);
        AssertUtil.notNull(decrLog, "退款失败，该订单还未支付");
        //2.并判断退款积分是否一致
        AssertUtil.isTrue(decrLog.getAmount().equals(intergralVo.getValue()), "退款金额不能大于支付金额");
        //3.增加积分
        int incrRow = usableIntegralMapper.addIntergral(intergralVo.getUserId(), decrLog.getAmount());
        AssertUtil.isTrue(incrRow > 0, "积分退款失败");
        //4.记录积分变动日志
        AccountLog accountLog = this.buildAccountLog(intergralVo, AccountLog.TYPE_INCR);
        int logSaveRow = accountLogMapper.insert(accountLog);
        AssertUtil.isTrue(logSaveRow > 0, "积分退款日志记录失败");
        return true;
    }

    //构建日志记录对象
    private AccountLog buildAccountLog(OperateIntergralVo intergralVo, int type) {
        AccountLog accountLog = new AccountLog();
        //交易流水号
        String tradeNo = String.valueOf(idGenerateUtil.nextId());
        accountLog.setTradeNo(tradeNo);
        //订单号
        accountLog.setOutTradeNo(intergralVo.getOutTradeNo());
        //用户ID
        accountLog.setUserId(intergralVo.getUserId());
        //类型：扣除积分
        accountLog.setType(type);
        //扣除积分
        accountLog.setAmount(intergralVo.getValue());
        //备注信息
        accountLog.setInfo(intergralVo.getInfo());
        return accountLog;
    }
}
